<?php
/**
* MetaBase software is commercially licensed
* visit http://www.pronique.com/software/metabase
* for licensing information
*/
if ( !defined( 'PS_METABASE_PATH' ) ) {        define( 'PS_METABASE_PATH', dirname( __FILE__) );
}

?>
<?php
/**
 * Super Simple and Convenient Persistent Storage Engine
 */ 
class ps_MetaBase {
  protected $data;  // In Memory Data Hash
  protected $driver; // ps_MetaBase_Driver
  protected $filters; // array of ps_MetaBase_Filter objects
  protected $debug = 0; // Level 0-9 Nine being the most 
  protected $defaultDriverConfig = array(
	'name'=>'Filesystem'
  );
  protected $config = array();

  /**
   * Class Constructor
   */
  public function __construct( $config=array(), $debugLevel=0 ) {
	if ( $debugLevel > 0) {
		$this->debug = $debugLevel;
		$this->debugger( 'Debugger level set to ' . $debugLevel );
	}

	if ( is_array( $config )) {
		$this->applyConfig( $config );
	}
	if ( isset( $config['driver'] ) ) {
	$this->loadDriver( $config['driver'] );
	} else  {
	$this->loadDriver( NULL );
	}
	if ( isset( $config['filters'] ) && count( $config['filters'] ) > 0 ) {
		foreach ( $config['filter'] as $name=>$config ) {
			$this->loadFilter( $name, $config );
		}
	}
  }

  /**
   * Quick Fetch no persistence
   */
  public function Fetch( $key ) {
	$this->debugger('->Fetch( ' . $key . ' )');
	return $this->data[$key];
  }

  /**
   * Fetch persistent stored data by $key
   * returns data or false if key is non-existent
   */
  public function pFetch( $key ) {
	$this->debugger('->pFetch( ' . $key . ' )');
	return $this->data[$key] = $this->driver->fetch( $key );
  }

  /**
   * Quick Store no persistence
   */
  public function Store( $key, $data ) {
	$this->debugger('->Store( ' . $key . ' )');
	$this->data[$key] = $data;
  }

  /**
   * Persistently Store $data by $key
   */
  public function pStore( $key, $data ) {
	$this->debugger('->pStore( ' . $key . ' )');
	$this->data[$key] = $data;
	return $this->driver->store($key, $data);
	
  }

  /**
   * load instance of ps_MetaBase_Driver_* into class
   */
  protected function loadDriver( $driverConfig ) {
    if ( !$driverConfig ) { 
	$this->debugger('Loading Default Driver Config', 9);
	$driverConfig = $this->defaultDriverConfig;
    }
    $classPrefix  = 'ps_MetaBase_Driver_';
    $driverClass = $classPrefix . $driverConfig['name'];
    $this->driver = new $driverClass( $driverConfig );	
  }

  /**
   * load instance of ps_MetaBase_Filter_* into $filters chain
   */
  protected function loadFilter( $name, $filterConfig ) {
    $this->debugger('Registering ' . $name . ' Filter', 9);
    $classPrefix  = 'ps_MetaBase_Filter_';
    $filterClass = $classPrefix . $name;
    $this->filters[$name] = new $filterClass( $filterConfig );	
    $this->filters[$name]->init();
  }

  /**
   * Merge config over the top of default config
   */
  protected function applyConfig( $config ) {
        $this->config = array_merge( $this->config, $config );
  } 

  /** 
   * debugger/logger method
   */
  protected function debugger( $data, $level=9 ) {
	if ( $level <= $this->debug ) {
		error_log (">>>>>>>>>>> Debug: " . $data );
        }
  }
}
?>
<?php
/** 
 * MetaBase_Driver Interface
 * All drivers must conform to this interface
 */
interface ps_MetaBase_iDriver {
  //function store();
  //function fetch();
}
?>
<?php
/** 
  * Parent Class for all MetaBase Drivers
  */
class ps_MetaBase_Driver {
  protected $config = array();

  function __construct( $config ) {
    $this->applyConfig( $config );
  }

  /**
   * Merge config over the top of default config
   */
  protected function applyConfig( $config ) {
	$this->config = array_merge( $this->config, $config );
  }

  /**
   * Simple Probability Test
   */
  protected function probability($prob, $div = 100) {
   $random = mt_rand( 1, $div );
    return $random <= $prob;
  }

  /**
   * Get Config by value or return entire thing
   */	
  public function getConfig( $key='' ) {
	if ($key) {
		return $this->config[$key];
	} else {
		return $this->config;
	}
  }
}
?>
<?php
class ps_MetaBase_Driver_Filesystem extends ps_MetaBase_Driver implements ps_MetaBase_iDriver {
  protected $config = array(
	'name'=>'Filesystem',
	'namespace'=>'SweetApp',
	/* 'path'=>'./cache', */
	'lifetime'=>60, // Expire files older than # seconds, exclude or set to 0 for infinite
	'purgeProbability'=>1,
	'purgeDivisor'=>100
  );
  public function __construct( $config ) {
    $this->applyConfig( $config );
    if ( !isset( $this->config['path'] ) ) { $this->config['path'] = PS_METABASE_PATH . '/cache'; }

    $this->initCache();

    if ( $this->config['lifetime'] > 0 ) {
      $this->pergeCache();
    }
  }
  public function store( $key, $data ) {
    $key = $this->getNamespace( '-' ) . $key;
    $fh = fopen( $this->getFilePath( $key ), 'w+' );
    fwrite($fh, serialize( $data ) );
    fclose( $fh );
  }

  public function fetch( $key ) {
    $key = $this->getNamespace( '-' ) . $key;
    if ( file_exists( $this->getFilePath( $key ) ) ) {
	return unserialize( file_get_contents( $this->getFilePath( $key ), 'r' )) ;
    } else { return false;}
  }
  public function pergeCache() {
    //TODO Delete all .pstore cache files in cache directory
    if ( $this->probability( $this->config['purgeProbability'], $this->config['purgeDivisor'] ) ) {
      $this->purgeOlder(  $this->getPath(), $this->config['lifetime'] );
    }
  }

  /** 
   * Delete Cache files that are more than $seconds old
   */
  protected function purgeOlder( $dir, $seconds=86400 ) {
    if( !$dirhandle = @opendir( $dir ) ) {
      return;
    }

    while( false !== ( $filename = readdir($dirhandle) ) ) {
      if( substr($filename, -7 ) == '.pstore' ) {
        $filename =  $dir . '/' . $filename;
        if( @filemtime($filename) < ( time() - $seconds ) ) {
		@unlink($filename);
	}
      }
    }
  }

  protected function initCache() {
    if ( !file_exists( $this->getPath() ) ) {
	mkdir( $this->getPath(), 0700 );

	$this->plantClue();
	if ( !file_exists( $this->getPath() ) ) {	
	   echo 'Unable to create ' . $this->getPath() . 'directory .  Check permissions';
	   exit;
  	}
    }
  }

  protected function plantClue() {
    $fh = fopen( $this->getPath() . '/README', 'w+');
    fwrite( $fh, 'This directory was created by the ps_MetaBase PHP library and is used for persistent caching to disk.');
    fclose( $fh );
  }

  /** 
   * Extract namespace from driver config
   * Namespaces are optional for drivers and good practice for shared persistent stores
   */
  protected function getNamespace( $append=NULL ) {
	if ( $this->config['namespace'] ) {
	  $namespace = $this->config['namespace'];
	}
	if ( $append ) { 
	  return $namespace . $append;
	}
	return $namespace;
  }
   
  /** 
   * Get the path to the filesystem store
   */
  protected function getPath() {
	return $this->config['path'];
  }

  /**
   * returns hashed filename with .pstore extension
   */
  protected function getFilename( $key ) {
	return  sha1( $key ) . '.pstore';
  }
  protected function getFilePath( $key ) {
	$filename = $this->getFilename( $key );
	$presplit = substr( $filename, 0, 2);
	// Big Filesystem
	// return $this->getPath() . '/' . $presplit  . '/' . $filename;
	return $this->getPath() . '/' . $filename;
  }
}
?>
<?php
class ps_MetaBase_Driver_BigFilesystem extends ps_Metabase_Driver_Filesystem 
	implements ps_MetaBase_iDriver {
  protected function initCache() {
    parent::initCache();
    for ( $i = 0; $i <= 255; $i++ ) {
        $hexdir = str_pad( dechex( $i ) , 2, '0', STR_PAD_LEFT );
    	if ( !file_exists( $this->getPath() . '/' . $hexdir ) ) {
             mkdir( $this->getPath(). '/' . $hexdir , 0700 );
    	}
    }
  }

  protected function getFilePath( $key ) {
        $filename = $this->getFilename( $key );
        $presplit = substr( $filename, 0, 2);        // Big Filesystem
        return $this->getPath() . '/' . $presplit  . '/' . $filename;
  }
}
?>
<?php
class ps_MetaBase_Filter {
  protected $config;

  function __construct( $config ) {
    $this->applyConfig( $config );
  }

  /**
   * Merge config over the top of default config
   */
  protected function applyConfig( $config ) {
        $this->config = array_merge( $this->config, $config );
  }
}
?>
<?php
/**
 * Every basic filter interface
 */
interface ps_MetaBase_iFilter {
  function init();
  function in( $data );
  function out( $data );
}
?>
<?php
/**
 * An example ps_MetaBase_Filter Class
 * This one does nothing
 */
class ps_MetaBase_Filter_Mock extends ps_MetaBase_Filter implements ps_MetaBase_iFilter {
	/**
	 * Just a wakeup call executed when this filter
	 * is activated and loaded into the chain
	 */
	function init() {

	}

	/**
	 * in() receives the $data before it's stored and
	 * must return $data or a modified form of it.
	 *
	 * out() should mirror or inverse the operations performed here
	 */
	function in( $data ) {
		return $data;
	}

	/**
	 * out() Sees the data after it's fetched from storage and
	 * must return $data or a modified form of it.
	 *
	 * This method should mirror or inverse the operations performed by in()
	 */
	function out( $data ) {
		return $data;
	}
}
?>
